/*
 *  Copyright (C) 2008-2009 Team XBMC http://www.xbmc.org
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package org.xbmc.eventclient;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;

/**
 * XBMC Event Client Class
 * 
 * Implements an XBMC-Client. This class can be used to implement your own
 * application which should act as a Input device for XBMC. Also starts a
 * Ping-Thread, which tells the XBMC EventServer that the client is alive.
 * Therefore if you close your application you SHOULD call stopClient()!
 * 
 * @author Stefan Agner
 * 
 */
public class XBMCClient {

	/**
	 * 
	 */
	private boolean hasIcon = false;
	/**
	 * 
	 */
	private PingThread oPingThread;
	/**
	 * 
	 */
	private byte iconType = Packet.ICON_PNG;
	/**
	 * 
	 */
	private byte[] iconData;
	/**
	 * 
	 */
	private InetAddress hostAddress;
	/**
	 * 
	 */
	private int hostPort;

	/**
	 * Starts a XBMC EventClient.
	 * 
	 * @param hostAddress
	 *            Address of the Host running XBMC
	 * @param hostPort
	 *            Port of the Host running XBMC (default 9777)
	 * @param deviceName
	 *            Name of the Device
	 * @param iconFile
	 *            Path to the Iconfile (PNG, JPEG or GIF)
	 * @throws IOException
	 *             if iconFile not readable
	 */
	public XBMCClient(final InetAddress hostAddress, final int hostPort,
			final String deviceName, final String iconFile) throws IOException {
		byte iconType = Packet.ICON_PNG;
		// Assume png as icon type
		if (iconFile.toLowerCase().endsWith(".jpeg")) {
			iconType = Packet.ICON_JPEG;
		}
		if (iconFile.toLowerCase().endsWith(".jpg")) {
			iconType = Packet.ICON_JPEG;
		}
		if (iconFile.toLowerCase().endsWith(".gif")) {
			iconType = Packet.ICON_GIF;
		}

		// Read the icon file to the byte array...
		FileInputStream iconFileStream = new FileInputStream(iconFile);
		byte[] iconData = new byte[iconFileStream.available()];
		iconFileStream.read(iconData);

		hasIcon = true;

		// Call start-Method...
		startClient(hostAddress, hostPort, deviceName, iconType, iconData);
	}

	/**
	 * Starts a XBMC EventClient.
	 * 
	 * @param hostAddress
	 *            Address of the Host running XBMC
	 * @param hostPort
	 *            Port of the Host running XBMC (default 9777)
	 * @param deviceName
	 *            Name of the Device
	 * @param iconType
	 *            Type of the icon file (see Packet.ICON_PNG, Packet.ICON_JPEG
	 *            or Packet.ICON_GIF)
	 * @param iconData
	 *            The icon itself as a Byte-Array
	 * @throws IOException
	 *             if iconFile not readable
	 */
	public XBMCClient(final InetAddress hostAddress, final int hostPort,
			final String deviceName, final byte iconType, final byte[] iconData)
			throws IOException {
		hasIcon = true;
		startClient(hostAddress, hostPort, deviceName, iconType, iconData);
	}

	/**
	 * Starts a XBMC EventClient without an icon.
	 * 
	 * @param hostAddress
	 *            Address of the Host running XBMC
	 * @param hostPort
	 *            Port of the Host running XBMC (default 9777)
	 * @param deviceName
	 *            Name of the Device
	 * @throws IOException
	 *             if iconFile not readable
	 */
	public XBMCClient(final InetAddress hostAddress, final int hostPort,
			final String deviceName) throws IOException {
		hasIcon = false;
		byte iconType = Packet.ICON_NONE;
		byte[] iconData = null;
		startClient(hostAddress, hostPort, deviceName, iconType, iconData);
	}

	/**
	 * Starts a XBMC EventClient.
	 * 
	 * @param hostAddress
	 *            Address of the Host running XBMC
	 * @param hostPort
	 *            Port of the Host running XBMC (default 9777)
	 * @param deviceName
	 *            Name of the Device
	 * @param iconType
	 *            Type of the icon file (see Packet.ICON_PNG, Packet.ICON_JPEG
	 *            or Packet.ICON_GIF)
	 * @param iconData
	 *            The icon itself as a Byte-Array
	 * @throws IOException
	 *             if iconFile not readable
	 */
	private void startClient(final InetAddress hostAddress, final int hostPort,
			final String deviceName, final byte iconType, final byte[] iconData)
			throws IOException {
		// Save host address and port
		this.hostAddress = hostAddress;
		this.hostPort = hostPort;
		this.iconType = iconType;
		this.iconData = iconData;

		// Send Hello Packet...
		PacketHELO p;
		if (hasIcon) {
			p = new PacketHELO(deviceName, iconType, iconData);
		} else {
			p = new PacketHELO(deviceName);
		}

		p.send(hostAddress, hostPort);

		// Start Thread (for Ping packets...)
		oPingThread = new PingThread(hostAddress, hostPort, 20000);
		oPingThread.start();
	}

	/**
	 * Stops the XBMC EventClient (especially the Ping-Thread).
	 * 
	 * @throws IOException
	 *             on network communication error
	 */
	public final void stopClient() throws IOException {
		// Stop Ping-Thread...
		oPingThread.giveup();
		oPingThread.interrupt();

		PacketBYE p = new PacketBYE();
		p.send(hostAddress, hostPort);
	}

	/**
	 * Displays a notification window in XBMC.
	 * 
	 * @param title
	 *            Message title
	 * @param message
	 *            The actual message
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendNotification(final String title, final String message)
			throws IOException {
		PacketNOTIFICATION p;
		if (hasIcon) {
			p = new PacketNOTIFICATION(title, message, iconType, iconData);
		} else {
			p = new PacketNOTIFICATION(title, message);
		}
		p.send(hostAddress, hostPort);
	}

	/**
	 * Sends a Button event.
	 * 
	 * @param code
	 *            raw button code (default: 0)
	 * @param repeat
	 *            this key press should repeat until released (default: 1) Note
	 *            that queued pressed cannot repeat.
	 * @param down
	 *            if this is 1, it implies a press event, 0 implies a release
	 *            event. (default: 1)
	 * @param queue
	 *            a queued key press means that the button event is executed
	 *            just once after which the next key press is processed. It can
	 *            be used for macros. Currently there is no support for time
	 *            delays between queued presses. (default: 0)
	 * @param amount
	 *            unimplemented for now; in the future it will be used for
	 *            specifying magnitude of analog key press events
	 * @param axis
	 *            the axis
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendButton(final short code, final boolean repeat,
			final boolean down, final boolean queue, final short amount,
			final byte axis) throws IOException {
		PacketBUTTON p = new PacketBUTTON(code, repeat, down, queue, amount,
				axis);
		p.send(hostAddress, hostPort);
	}

	/**
	 * Sends a Button event.
	 * 
	 * @param mapName
	 *            a combination of map_name and button_name refers to a mapping
	 *            in the user's Keymap.xml or Lircmap.xml. map_name can be one
	 *            of the following:
	 *            <ul>
	 *            <li>"KB" => standard keyboard map ( <keyboard> section )</li>
	 *            <li>"XG" => xbox gamepad map ( <gamepad> section )</li>
	 *            <li>"R1" => xbox remote map ( <remote> section )</li>
	 *            <li>"R2" => xbox universal remote map ( <universalremote>
	 *            section )</li>
	 *            <li>"LI:devicename" => LIRC remote map where 'devicename' is
	 *            the actual device's name</li>
	 *            </ul>
	 * @param buttonName
	 *            a button name defined in the map specified in map_name. For
	 *            example, if map_name is "KB" refering to the <keyboard>
	 *            section in Keymap.xml then, valid button_names include
	 *            "printscreen", "minus", "x", etc.
	 * @param repeat
	 *            this key press should repeat until released (default: 1) Note
	 *            that queued pressed cannot repeat.
	 * @param down
	 *            if this is 1, it implies a press event, 0 implies a release
	 *            event. (default: 1)
	 * @param queue
	 *            a queued key press means that the button event is executed
	 *            just once after which the next key press is processed. It can
	 *            be used for macros. Currently there is no support for time
	 *            delays between queued presses. (default: 0)
	 * @param amount
	 *            unimplemented for now; in the future it will be used for
	 *            specifying magnitude of analog key press events
	 * @param axis
	 *            the axis
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendButton(final String mapName, final String buttonName,
			final boolean repeat, final boolean down, final boolean queue,
			final short amount, final byte axis) throws IOException {
		PacketBUTTON p = new PacketBUTTON(mapName, buttonName, repeat, down,
				queue, amount, axis);
		p.send(hostAddress, hostPort);
	}

	/**
	 * Sets the mouse position in XBMC.
	 * 
	 * @param x
	 *            horitontal position ranging from 0 to 65535
	 * @param y
	 *            vertical position ranging from 0 to 65535
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendMouse(final int x, final int y) throws IOException {
		PacketMOUSE p = new PacketMOUSE(x, y);
		p.send(hostAddress, hostPort);
	}

	/**
	 * Sends a ping to the XBMC EventServer.
	 * 
	 * 
	 * @throws IOException
	 *             on network communication error
	 */
	public final void ping() throws IOException {
		PacketPING p = new PacketPING();
		p.send(hostAddress, hostPort);
	}

	/**
	 * Tells XBMC to log the message to xbmc.log with the loglevel as specified.
	 * 
	 * @param loglevel
	 *            the loglevel, follows XBMC standard.
	 *            <ul>
	 *            <li>0 = DEBUG</li>
	 *            <li>1 = INFO</li>
	 *            <li>2 = NOTICE</li>
	 *            <li>3 = WARNING</li>
	 *            <li>4 = ERROR</li>
	 *            <li>5 = SEVERE</li>
	 *            </ul>
	 * @param logmessage
	 *            the message to log
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendLog(final byte loglevel, final String logmessage)
			throws IOException {
		PacketLOG p = new PacketLOG(loglevel, logmessage);
		p.send(hostAddress, hostPort);
	}

	/**
	 * Tells XBMC to do the action specified, based on the type it knows were it
	 * needs to be sent.
	 * 
	 * @param actionmessage
	 *            Actionmessage (as in scripting/skinning)
	 * @throws IOException
	 *             on network communication error
	 */
	public final void sendAction(final String actionmessage) throws IOException {
		PacketACTION p = new PacketACTION(actionmessage);
		p.send(hostAddress, hostPort);
	}

	/**
	 * Implements a PingThread which tells XBMC EventServer that the Client is
	 * alive (this should be done at least every 60 seconds!
	 * 
	 * @author Stefan Agner
	 * 
	 */
	@SuppressWarnings("hiding")
	class PingThread extends Thread {

		/**
		 * 
		 */
		private final InetAddress hostAddress;
		/**
		 * 
		 */
		private final int hostPort;
		/**
		 * 
		 */
		private final int sleepTime;
		/**
		 * 
		 */
		private boolean giveup = false;

		/**
		 * @param hostAddress
		 *            the host address
		 * @param hostPort
		 *            the port of the host
		 * @param sleepTime
		 *            the time to sleep
		 */
		public PingThread(final InetAddress hostAddress, final int hostPort,
				final int sleepTime) {
			super("XBMC EventClient Ping-Thread");
			this.hostAddress = hostAddress;
			this.hostPort = hostPort;
			this.sleepTime = sleepTime;
		}

		/**
		 * gives up.
		 */
		public void giveup() {
			giveup = true;
		}

		@Override
		public void run() {
			while (!giveup) {
				try {
					PacketPING p = new PacketPING();
					p.send(hostAddress, hostPort);
				} catch (IOException e) {

					e.printStackTrace();
				}
				try {
					Thread.sleep(sleepTime);
				} catch (InterruptedException e) {
					/* */
				}
			}
		}
	}
}
